package edu.ucla.multicastim.audio;

/*
 * Experimental multiple party voice chat
 */

import java.io.*;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.net.UnknownHostException;

import javax.sound.sampled.*;

import edu.ucla.multicastim.util.NetworkAddressUtil;

public class MultiAudioChat {

	private ByteArrayOutputStream outgoing;
	private ByteArrayOutputStream[] incoming;
	private AudioFormat format;
	private float sampleRate;
	private int sampleSizeInBits;
	private int channels;
	private boolean signed;
	private boolean bigEndian;
	private boolean update;
	private boolean safe;

	public DatagramSocket socket;
	private String[] ips;
	private InetAddress[] address;
	private DatagramSocket[] sockets;

	private int count;

	private static int pThreadCount;

	private lThread l;
	private pThread[] playThread;
	private rThread r;
	private sThread s;

	public MultiAudioChat() {
		count = 1;
		pThreadCount = Integer.MAX_VALUE;
		try {
			socket = new DatagramSocket(55556);
		} catch (SocketException e) {
			System.out.println(e.getMessage());
		}
	}

	public void setMembershipList(String[] memberList) {

		String[] localMemList = getMemberList(memberList);

		// so that we know how many pThreads are running
		int temp = localMemList.length;

		// notify the send thread that we need to update memberList
		update = true;

		// wait for the send thread to end and for all the play threads to die
		while (!safe && (pThreadCount < temp)) {
			;
		}

		initialize(localMemList);
		update = false;

		if (pThreadCount == Integer.MAX_VALUE) {
			r = new rThread();
			r.start();
		}

		l = new lThread();
		l.start();

		pThreadCount = 0;
	}

	class lThread extends Thread {
		public void run() {
			listen();
		}
	}

	class pThread extends Thread {

		private ByteArrayOutputStream source;

		public pThread(ByteArrayOutputStream baos) {
			this.source = baos;
		}

		public void run() {
			try {
				while (!update) {
					playBack(source);
				}

			} catch (IOException e) {
				System.err.println("I/O exception: unable to playback");
				System.exit(-1);
			}
			synchronized (this) {
				pThreadCount++;
			}
		}
	}

	private class rThread extends Thread {
		public void run() {
			record();
		}
	}

	class sThread extends Thread {
		public void run() {
			while (true) {
				try {
					if (update) {
						safe = true;
						continue;
					} else {
						send();
					}

				} catch (InterruptedException e) {
					System.err.println("Interrupted exception");
					System.exit(-1);
				}
			}
		}
	}

	private void record() {
		DataLine.Info info = new DataLine.Info(TargetDataLine.class, format);
		TargetDataLine line = null;
		try {
			line = (TargetDataLine) AudioSystem.getLine(info);
			line.open(format);
		} catch (LineUnavailableException e) {
			System.err.println("Line unavailable");
			System.exit(-1);
		}

		// starts capturing
		line.start();

		int bufferSize = (int) format.getSampleRate() * format.getFrameSize();

		byte buffer[] = new byte[bufferSize];
		outgoing = new ByteArrayOutputStream();
		int c = 1;

		// this is to make sure that there is audio data to send before spawning
		// the send threads
		int count = line.read(buffer, 0, buffer.length);
		if (count > 0) {
			outgoing.write(buffer, 0, count);
			System.out.println("Now recording for " + c++ + " seconds");
		}

		s = new sThread();
		s.start();

		while (true) {
			count = line.read(buffer, 0, buffer.length);
			if (count > 0) {
				outgoing.write(buffer, 0, count);
				System.out.println("Now recording for " + c + " seconds");
			}
			c++;
		}
	}

	private void send() throws InterruptedException {
		byte audio[] = outgoing.toByteArray();
		outgoing.reset();

		int byteToSend = audio.length;
		int remain = audio.length;

		// Opens datagram sockets
		try {
			for (int i = 0; i < address.length; i++) {
				sockets[i] = new DatagramSocket();
				sockets[i].connect(address[i], 55556);
			}
		} catch (SocketException e) {
			System.err.println("Socket exception: unable to open socket");
			System.exit(-1);
		}

		while (remain > 0) {
			if (remain > 1024) {
				DatagramPacket packet;
				try {

					for (int i = 0; i < ips.length; i++) {
						packet = new DatagramPacket(audio, byteToSend - remain,
								1024, address[i], 55556);
						sockets[i].send(packet);
						System.out.println("remaining bytes: " + remain);
						count++;
					}
					remain -= 1024;
					System.out.println("Packet #" + count + " sent");
				} catch (IOException e) {
					System.err.println("I/O exception: unable to send packet");
					System.exit(-1);
				}
			} else {
				DatagramPacket packet;
				try {
					for (int i = 0; i < ips.length; i++) {
						packet = new DatagramPacket(audio, byteToSend - remain,
								remain, address[i], 55556);
						sockets[i].send(packet);
						System.out.println("remaining bytes: " + remain);
						count++;
					}
					remain = 0;
				} catch (IOException e) {
					System.err.println("I/O exception: unable to send packet");
					System.exit(-1);
				}
			}
		}
		for (int i = 0; i < ips.length; i++) {
			sockets[i].close();
		}
	}

	/**
	 * Accepts audio from a remote host and plays it
	 */
	private void listen() {

		boolean inputIsEmpty = true;
		boolean threadStarted = false;

		int counter = 0;
		byte[] temp = new byte[1024];

		DatagramPacket packet = null;

		while (!update) {
			if (!threadStarted && counter > 50) {
				inputIsEmpty = false;
			}

			packet = new DatagramPacket(temp, temp.length);

			try {
				System.out.println("Now listening...");
				socket.receive(packet);
				InetAddress senderAddress = packet.getAddress();
				String org = packet.getAddress().toString();
				for (int i = 0; i < ips.length; i++) {
					if (org.equals(address[i].toString())) {
						incoming[i].write(packet.getData());
					}
					
					// If it's NOT the person who sent us the packet,
					// forward it
					// to membershipList
					if (!senderAddress.equals(address[i])) {
						DatagramSocket forwardingSocket = null;
						try {
							DatagramPacket forwardedPacket = new DatagramPacket(
									packet.getData(), packet.getData().length,
									address[i], 55556);
							forwardingSocket = new DatagramSocket();
							forwardingSocket.send(forwardedPacket);
						} catch (SocketException e) {
							e.printStackTrace();
						} catch (IOException e) {
							e.printStackTrace();
						}
						forwardingSocket.close();

					}
				}

				System.out.println("Packet received from " + org + ", count = "
						+ counter++);
			} catch (IOException e) {
				System.err.println("Unable to receive packet");
				System.exit(-1);
			}

			if (!threadStarted && !inputIsEmpty) {
				threadStarted = true;
				for (int i = 0; i < ips.length; i++) {
					playThread[i] = new pThread(incoming[i]);
					playThread[i].start();
				}
			}
		}
	}

	/**
	 * Plays audio from a ByteArrayOutputStream
	 * 
	 * @param baos
	 * @throws IOException
	 */
	private void playBack(ByteArrayOutputStream baos) throws IOException {

		byte buffer[] = baos.toByteArray();
		baos.reset();

		AudioInputStream ais = new AudioInputStream(new ByteArrayInputStream(
				buffer), format, buffer.length / format.getFrameSize());

		DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
		SourceDataLine pline = null;
		try {
			pline = (SourceDataLine) AudioSystem.getLine(info);
			pline.open(format);
		} catch (LineUnavailableException e) {
			System.err.println("Line unavailable");
			System.exit(-1);
		}

		pline.start();

		int count;
		while ((count = ais.read(buffer, 0, buffer.length)) != -1) {
			if (count > 0) {
				pline.write(buffer, 0, count);
			}
		}
		pline.drain();
		pline.close();
	}

	/**
	 * For playback on local machine
	 * 
	 * @throws IOException
	 */
	private void playBack() throws IOException {
		byte audio[] = outgoing.toByteArray();
		outgoing.reset();
		AudioInputStream ais = new AudioInputStream(new ByteArrayInputStream(
				audio), format, audio.length / format.getFrameSize());

		DataLine.Info info = new DataLine.Info(SourceDataLine.class, format);
		SourceDataLine pline = null;
		try {
			pline = (SourceDataLine) AudioSystem.getLine(info);
			pline.open(format);
		} catch (LineUnavailableException e) {
			System.err.println("Line unavailable");
			System.exit(-1);
		}

		pline.start();

		int bufferSize = (int) format.getSampleRate() * format.getFrameSize();
		byte buffer[] = new byte[bufferSize];

		int count;
		while ((count = ais.read(buffer, 0, buffer.length)) != -1) {
			if (count > 0) {
				pline.write(buffer, 0, count);
			}
		}
		pline.drain();
		pline.close();
	}

	/**
	 * 
	 * Specify audio format, create new ByteArrayOutputStream for each member,
	 * copy memberList and store it in local ips, create array of InetAddress
	 * corresponding to each address in memberList, and create a output socket
	 * for each member.
	 * 
	 * @param memberList
	 */
	private void initialize(String[] memberList) {
		// specify audio format
		sampleRate = 8000;
		sampleSizeInBits = 8;
		channels = 1;
		signed = true;
		bigEndian = true;
		format = new AudioFormat(sampleRate, sampleSizeInBits, channels,
				signed, bigEndian);
		playThread = new pThread[memberList.length];

		// network stuff
		incoming = new ByteArrayOutputStream[memberList.length];
		ips = new String[memberList.length];
		address = new InetAddress[memberList.length];
		sockets = new DatagramSocket[memberList.length];

		for (int i = 0; i < memberList.length; i++) {

			incoming[i] = new ByteArrayOutputStream(1024);
			ips[i] = memberList[i];
			try {
				address[i] = InetAddress.getByName(memberList[i]);
				sockets[i] = new DatagramSocket();
			} catch (UnknownHostException e) {
				System.err.println(e.getMessage());
				System.exit(-1);
			} catch (SocketException e) {
				System.err.println(e.getMessage());
				System.exit(-1);
			}
			sockets[i].connect(address[i], 55556);
		}
	}

	private String[] getMemberList(String[] memberList) {
		String[] temp;
		String myAddress = NetworkAddressUtil.getLocalHostAddress();

		int index = -1;

		for (int i = 0; i < memberList.length; i++) {
			if (myAddress.equals(memberList[i])) {
				index = i;
				break;
			}
		}

		if (index == -1) {
			return memberList;
		} else {
			temp = new String[memberList.length - 1];
		}
		int k = 0;
		for (int i = 0; i < temp.length; i++) {
			if (k == index) {
				temp[i] = memberList[++k];
			} else {
				temp[i] = memberList[k++];
			}
		}
		return temp;
	}
}